/**
Copyright 2014 Hiroyuki Tominaga

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// REVISION LOG ENTRY
//
//	File name  : scramble-key-task.ajs
// Revision By: (C) Copyright 2014 Hiroyuki Tominaga, All Rights Reserved.
//
//	Revised on : 2014-06-01 05:18:43
//	Summary    :
//
//

var libraryPackages = new JavaImporter(
	java.lang,
	java.io,
	java.util,
	java.text,
	com.mylyane.util
);

with (libraryPackages) {

	// --------------------------------------------------------------------------------------------
	//
	//                                          Global vars.
	//
	// --------------------------------------------------------------------------------------------
	/* immutable vars */
	var RACFP = "$%&'()*+,-./0123456789;<>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~";
	var _random = new Random();

	var UID_SIZE_DEFAULT = 5;
	var BUNDLED_PROP = "<BUNDLED_PROP>";
	var KEY_MAP_FILE_NAME = "scrambled-map.properties";

	var CACHE_FILE_DEFAULT = "ajs-cache/laststamp";
	var LOG_PREFIX = " **** scramble-key-task.ajs **** - ";

	var ATTR_FORCE = "force";
	var ATTR_VERBOSE = "verbose";
	var ATTR_LASTMOD_CACHE = "lastmod-cache";
	var ATTR_UID_SIZE = "uid-size";
	var ATTR_OUT_DIR = "outdir";

	/* mutable vars */
	var g_LastmodCache = null;
	var g_Outdir = null;
	var g_Uid_size = UID_SIZE_DEFAULT;

	var g_Force = false;
	var g_Verbose = false;

	// --------------------------------------------------------------------------------------------
	//
	//                                       utility functions
	//
	// --------------------------------------------------------------------------------------------
	function getEValue(ename, index) {
		var e = getE(ename);
		return e !== null? e.get(index): null;
	}
	function getE(ename) {
		return elements.get(ename);
	}
	function getSimpleName(name) {
		var x = name.lastIndexOf(".");
		return name.substring(0, x);
	}
	function extractName(str_path) {
		var x = str_path.lastIndexOf(File.separator);
		return str_path.substring(x + 1);
	}
	function convertStringArray(ename) {
		var arraylist = elements.get(ename).get(0);
		return arraylist.toString().split(File.pathSeparator);
	}

	/**
	 * @param size 文字列長
	 */
	/* String */
	function GenerateKey(size) {
		var data_len = RACFP.length;
		var buffer = new StringBuilder(size);
		while (size--) {
			buffer.append( RACFP[_random.nextInt(data_len)] );
		}
		return buffer.toString();
	}

	/*Properties*/
	function LoadProperties(rel_path)
	{
		var prop = new Properties();
		try {
			var stream = new InputStreamReader(new FileInputStream(rel_path), "utf-8");
			prop.load(stream);
			stream.close();
		} catch (e) {
			print(e);
		}
		return prop;
	}
	/* void */
	function SaveProperties(properties, file, comment) {
		var out = null;
		try {
			out = new FileOutputStream( file );
			properties.store(out, comment);
		}
		catch (e) {
			print(e);
		} finally {
			if (out != null) {
				try { out.close(); } catch (e) {}
			}
		}
	}

	/**
	 * 階乗計算 n!
	 */
	function factorial(n) {
		var fact = 1;
		if (n == 0)
			return fact;
		else {
			for(var i = n; i > 0; i--)
				fact *= i;
			return fact;
		}
	}



	// --------------------------------------------------------------------------------------------
	//
	//                                       task functions
	//
	// --------------------------------------------------------------------------------------------
	/**
	 * <pre>
	 * "$%&'()*+,-./0123456789;<>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}~"
	 * の 88 文字をつかって key を難読化します. nPr = 88!/(88-size)! 通り
	 *
	 * NOTE: 2014-05-16 20:10:58
	 *    以前は 92 文字だったが, property file を 他の property file に埋め込むことを想定して,
	 *    '\' で escape される character を除外した.
	 * </pre>
	 * @param properties_arry property key を scramble する properties file 配列.
	 * @param uid_size        scramble key length.(3 以下では random 度合いが低すぎると思われるので注意が必要
	 * @return 処理結果の data を持った anonymouse object. ( { key_map: key_map, sets: sets }
	 */
	/*ScrambleKeyResult*/
	function scrambleKeyAll(/*String[]*/ properties_arry, /*int*/ uid_size)
	{
		var size;
		if (properties_arry === null || (size = properties_arry.length) === 0)
			return null;

		/*AbstractMap<String, String>*/
		var key_map = new HashMap(200, .8);
		/*AbstractMap<File, Properties>*/
		var sets = new HashMap(size * 1.5, .8);

		var count = 0;
		var call_depth = 0;
		while (size--)
		{
			var file = properties_arry[size];
			var src_properties;

			try {
				src_properties = LoadProperties(file);
			} catch (e) {
				project.log(LOG_PREFIX + e);
				src_properties = null;
			}
			if (src_properties == null || src_properties.size() == 0)
				continue;

			print(" outer scramble: " + count++);
			// map properties to original file.
			sets.put(
				file,
				processScramble(src_properties, key_map, uid_size, call_depth)
			);
		}

		return { key_map: key_map, sets: sets };
	}

	/**
	 * @param src_properties       scramble 対象の source properties.
	 * @param key_map              scramble key と original key の関連を保持する map.
	 * @param uid_size             作成する random key の長さ.
	 * @param recurse_call_depth   再帰 call depth. 現実装では, depth 1 までに制限.
	 *
	 * @return source の key が scramble された結果の scrambled_properties.
	 */
	/* Properties */
	function processScramble(src_properties, key_map, uid_size, recurse_call_depth)
	{
		var keys = src_properties.keys();
	    var scrambled_properties = new Properties();
		while (keys.hasMoreElements()) {
			var original = keys.nextElement();
			var value = src_properties.get(original);
			// 埋め込まれた property data であることを識別する token がついているか?
			if (original.indexOf(BUNDLED_PROP) != -1 && recurse_call_depth === 0) {
			    value = caseInlineScramble(
			        value, key_map, uid_size, recurse_call_depth + 1
			    );
			    if (value == null) value = "";
			}
            // NOTE: 2014/01/29 間違いなく property key string とするため
            // original, suid ともに '"' で囲むことにした.
            var quote_original = "\"" + original + "\"";
            var suid;
            // original to suid の関連付けがまだない.
            if (!key_map.containsKey(quote_original)) {
                suid = GenerateKey(uid_size);
                var quote_suid = "\"" + suid + "\"";
                // original を key として suid を登録.
                key_map.put(quote_original, quote_suid);
            } else {
                // すでに original to suid が存在する.
                suid = key_map.get(quote_original).substring(1, uid_size + 1);
            }
			// original で関連付けられていた値を, suid を key として登録しなおす.
			scrambled_properties.put(suid, value);

			if (g_Verbose) {
				printVerbose(original, suid);
			}
		} // -- while (keys.hasMoreElements())
		return scrambled_properties;
	}

	/**
	 * <pre>
	 * String source を property data をみなして scramble します.
	 * see <A href="https://sharuru1107.unfuddle.com/a#/projects/1/tickets/by_number/2?cycle=true">Ticket #2</A>
	 * </pre>
	 * @param source   Unicode escape が含まれていない property data.
	 * @param key_map  scramble key と original key の関連を保持する map.
	 * @param uid_size 作成する random key の長さ.
	 *
	 * @return source の key が scramble された結果の property data string.
	 */
	/* String */
	function caseInlineScramble(
	    /*String*/ source, /*AbstractMap<String, String>*/ key_map, /*int*/ uid_size,
	    /*int*/ recurse_call_depth
	) {
	    var src_properties = new Properties();
	    try { // NOTE: 2014/05/20 1:25:37 not need close.
	        src_properties.load( new StringReader(source) );
	    }
	    catch (e) {
	        print(e);
	        return null;
	    }

		print(" inner scramble, enter depth=" + recurse_call_depth);
		var scrambled_properties = processScramble(src_properties, key_map, uid_size, recurse_call_depth);
		print(" inner scramble, exit  depth=" + recurse_call_depth);

	    try { // NOTE: 2014/05/20 1:25:37 not need close.
	        var sw = new StringWriter();
	        scrambled_properties.store(sw, null); /* null にすると comment は書き込まれない */
	        return sw.toString();
	    }
	    catch (e) {
	        print(e);
	        return null;
	    }
	}
	// 処理状況を print
	function printVerbose(original, suid) {
		var msg = "Scrambled, key=" + original + " -";
		var len = msg.length;
		var repeat = 88 - len;
		if (repeat > 0) {
			while (repeat--)
				msg += "-";
		}
		print(
			msg + " suid=" + suid
		);
	}


	/** <pre>
	 * cache された properties file の lastModified と渡された properties file の
	 * lastModified を比較.
	 *
	 * output directory の file が存在するかどうかも check.
	 * </pre>
	 */
	function IsNeedUpdate(files, dest)
	{
		var properties = LoadProperties( g_LastmodCache );
		var size = properties.size() - 1;
		if (size < 0 || size != files.length)
			return true;

		var checked = properties.get(ATTR_UID_SIZE);
		if (checked !== null && g_Uid_size != checked) {
			return true;
		}

		checked = 0;
		for (var i in files) {
			var file = new File(files[i]);
			var cache_tstamp = properties.get(file.getCanonicalPath());
			if (cache_tstamp != null) {
				checked++;
				var lval = Long.valueOf(cache_tstamp).longValue();
				if (file.lastModified() != lval) {
					return true;
				}
			}
			if (!new File(dest, file.getName()).exists())	/* output directory の file check. */
				return true;
		}
		if (!new File(dest, KEY_MAP_FILE_NAME).exists())	/* output directory の file check. */
			return true;

		return !(checked == size);
	}
	/**
	 * properties file の lastModified を cache.
	 */
	function cacheFileLastMod(files) {
		var properties = new Properties();
		var cache_file = new File(g_LastmodCache);
		var cache_dir = cache_file.getParentFile();
		if (!cache_dir.exists())
			cache_dir.mkdirs();
		for (var i in files) {
			var file = new File(files[i]);
			properties.put(file.getCanonicalPath(), file.lastModified().toString());
		}
		properties.put(ATTR_UID_SIZE, g_Uid_size.toString());
		SaveProperties(properties, cache_file, " scramble key task cache.");
	}

	function traceGet(name) {
		var attrval = attributes.get(name);
		project.log(LOG_PREFIX + name + "=" + attrval);
		return attrval;
	}
	/**
	 * task に設定された attribute の値をすべて取得.
	 */
	function fetchAttributes() {
		var attrval = traceGet(ATTR_LASTMOD_CACHE);
		if (attrval === null)
			attrval = CACHE_FILE_DEFAULT;
		g_LastmodCache = attrval;

		var attrval = traceGet(ATTR_UID_SIZE);
		if (attrval === null)
			attrval = UID_SIZE_DEFAULT;
		g_Uid_size = Integer.valueOf(attrval).intValue();

		var attrval = traceGet(ATTR_OUT_DIR);
		if (attrval !== null)
			g_Outdir = attrval;

		var attrval = traceGet(ATTR_FORCE);
		if (attrval !== null && attrval.length > 0) {
			g_Force = attrval == "true" || attrval == "on";
		}

		var attrval = traceGet(ATTR_VERBOSE);
		if (attrval !== null && attrval.length > 0)
			g_Verbose = attrval == "true" || attrval == "on";
	}

	// scramble の product を scramble destination directory に配置して終了.
	function doScrambleAndDeploy(files, dest)
	{
		/*ScrambleKeyResult key_map, sets*/
		var scramble = scrambleKeyAll(files, g_Uid_size);
		var fd = new File(dest);
		if (!fd.exists()) {
			fd.mkdirs();
			print(fd + " created.");
		}
		if (scramble.key_map.size() > 0) {
			var p = new Properties();
			p.putAll(scramble.key_map);
			SaveProperties(p, new File(dest, KEY_MAP_FILE_NAME), " Original to StringUID map.");
		}
		if (scramble.sets.size() > 0) {
			var it = scramble.sets.keySet().iterator();
			while (it.hasNext()) {
				var file = it.next();
				var name = extractName(file);
				var p = scramble.sets.get(file);
				SaveProperties(p, new File(dest, name), " Key scrambled properties.");
			}
		}
		/* print statistics */
		print(
			"scramble done, count=" + scramble.key_map.size() + ", uniqe id length=" + g_Uid_size
		);
		var fact = Math.ceil( factorial(RACFP.length) / factorial(RACFP.length - g_Uid_size) );
		print(
			"total number of combinations :" + NumberFormat.getInstance().format(fact)
		);
	}

	// ------------------------------------------------------------------------ [ENTRY POINT]
	/**
	* TASK ENTRY POINT.
	*/
	function fireTask()
	{
		project.log(LOG_PREFIX + "Property key scramble work at javascript [fireTask]");

		fetchAttributes();

		var dest = g_Outdir !== null? g_Outdir: getEValue("outdir", 0).toString();
		var files = convertStringArray("source");

		if (g_Force || IsNeedUpdate(files, dest) ) {
			doScrambleAndDeploy(files, dest);
			cacheFileLastMod(files);
		}
		else
			project.log(LOG_PREFIX + "This task is not necessary.");
	}

} // with (libraryPackages);


// --------------------------------------------------------------------------------------
/* scramblekey.js
*/
/* fire main function! */
fireTask();


